Обновить

I2P: Transparent implementation of EdDSA signature

Время на прочтение 5 min
Количество просмотров 14K

Recently, the electronic signature Ed25519, based on a type of elliptic curve proposed by Bernstein, has become increasingly popular. As the number of I2P nodes with this type of signature increased, it became necessary to support it in their I2P implementation, since Ed25519 is not included in popular cryptographic libraries. As a rule, ref10 varieties from the library are used SUPERCOP, implemented by Bernstein himself in assembler, and then ported to other languages. This implementation works well and quickly, but it has a major drawback - it is not clear. Indeed, if you look into the source code, you can see a large number of similar lines operating with many “magic” numbers, but it is not possible to understand what they mean without delving into the theory. The goal of this article is a mathematically transparent implementation of Ed22519, using only standard large number operations found in any cryptography library, at a speed sufficient for practical use in I2P.




How elliptic cryptography works


An implicit equation of a special type of plane curve called an elliptic curve is specified. A prime number is specified, forming a field of modulo residues. The coordinates of the curve points belong to this field, i.e. are non-negative integers not exceeding the modulus. An addition operation is specified on two points on the curve so that the new point also belongs to the curve. If you add a point to itself, you get multiplication by 2, if you add it again, then by 3, and so on, multiplication by an arbitrary number n. Also, along with the curve, a base point belonging to the curve is specified, operations on which are used in cryptography.


For cryptography, an arbitrary large number n is selected, which is the secret key, the base point is multiplied by it and the new point serves as the public key, which is then multiplied by the opposite side by another number for the purpose of key agreement or signature verification. Resilience is based on the current lack of a known method for determining the multiplier by point in a reasonable time



ed25519


Bershtein proposed using an elliptic curve with the following parameters.
Curve Equation:
, Where
Module:
q =
hence the name of the curve.

Base point:
(x, 4/5), where x is obtained by solving the equation (see below)
Its coordinates in explicit form:
x=15112221349535400772501151409588531511454012693041857206046113283949847762202,
y=46316835694926478169428394003475163141307993866256225615783033603165251855960.

Addition:
, Where

It should be noted that the division here is not the usual division with a remainder, but multiplication by the inverse modulus element, calculated according to Fermat's little theorem according to the formula , i.e., the operation of exponentiation modulo.

It is proposed to use only the coordinate as a public key y, if necessary, solving the equations of the curve for x, and calculating it using the formula .

Because the q representable in the form 8*K+5, then to calculate the square root of X you should use the formula . Indeed q = = , hence the square root .

Implementation


The code is completely located on by the address in the Signature.cpp file. Library functions are used to work with large numbers BN from openssl.
Let's create a class Ed25519 that implements the curve itself and contains the curve parameters calculated in its constructor. First of all, 3 methods are needed: addition, multiplication by a number and calculating x from a given y.
class Ed25519
{
//... 
    EDDSAPoint Sum (const EDDSAPoint& p1, const EDDSAPoint& p2) const
    EDDSAPoint Mul (const EDDSAPoint& p, const BIGNUM * e) const
    BIGNUM * RecoverX (const BIGNUM * y) const
//...
}


Due to the particular use of the addition operation and the complexity of the division operation, we reduce x and y to a common denominator (1+m)*(1-m), thereby getting rid of one division. As a result, the code for addition looks something like this:
// m = d*p1.x*p2.x*p1.y*p2.y
BN_mul (xx, p1.x, p2.x, m_Ctx); //p1.x*p2.x
BN_mul (yy, p1.y, p2.y, m_Ctx);  //p1.y*p2.y
BIGNUM * m = BN_dup (d);
BN_mul (m, m, xx, m_Ctx);
BN_mul (m, m, yy, m_Ctx);
// x = (p1.x*p2.y + p2.x*p1.y)*inv(1 + m)
// y = (p1.y*p2.y + p1.x*p2.x)*inv(1 - m)
// m1 = 1-m
BN_one (m1);
BN_sub (m1, m1, m);
// m = m+1
BN_add_word (m, 1);		
// y = (p1.y*p2.y + p1.x*p2.x)*m	
BN_add (y, xx, yy);
BN_mod_mul (y, y, m, q, m_Ctx);
// x = (p1.x*p2.y + p2.x*p1.y)*m1
BN_mul (yy, p1.x, p2.y, m_Ctx);
BN_mul (xx, p2.x, p1.y, m_Ctx);
BN_add (x, xx, yy);
// denominator m = m*m1	
BN_mod_mul (m, m, m1, q, m_Ctx);
Inv (m); 	
BN_mod_mul (x, x, m, q, m_Ctx); // x = x/m
BN_mod_mul (y, y, m, q, m_Ctx); // y = y/m


Also, for doubling (adding a point to itself), a separate Double method is implemented, since in this case p1.x = p2.x and p1.y = p2.y, which allows you to reduce the number of multiplications. In addition, the faster BN_sqr is used instead of BN_mul.
Multiplication released using the simplest method doubling and adding, those. go along the number bitwise from high to low, at each step double the value of the result, and if the bit is one, then add the point to be multiplied.
EDDSAPoint res {zero, one};
if (!BN_is_zero (e))
{
    int bitCount = BN_num_bits (e);
    for (int i = bitCount - 1; i >= 0; i--)
    {
        res = Double (res);
        if (BN_is_bit_set (e, i)) res = Sum (res, p);
    }
}	

Computing x from y is trivial.
First the radical expression:
BN_sqr (y2, y, m_Ctx); // y^2
// xx = (y^2 -1)*inv(d*y^2 +1) 
BN_mul (xx, d, y2, m_Ctx);
BN_add_word (xx, 1);
Inv (xx);
BN_sub_word (y2, 1);
BN_mul (xx, y2, xx, m_Ctx);

Then calculate the square root using the formula
// x = srqt(xx) = xx^(2^252-2)		
BN_mod_exp (x, xx, two_252_2, q, m_Ctx);


Signature and signature verification


This topic is quite interesting and extensive, so this issue will be devoted to separate article. At the same time, the Sign and Verify methods are implemented and used practically. Because if anyone is interested, you can look at the code. Here we will only list some features.
Like other electronic signature systems, the EdDSA signature is a pair of 32-byte numbers (R, S), therefore the length of the signature is 64 bytes.
Numbers presented in Little Endian.
SHA is used as a hash function-512.
When signing, a random number is not generated; instead, the right half of the SHA-512 hash of the secret key is used, combined with the data being signed..
Prime number is also used , used when choosing a base point, so that multiplying the base point by it would give zero.

Conclusion


Obviously, the slowest part of this implementation is the division in the addition operation. In fact, using modern advances in number theory and the specific parameters of EdDSA, it is possible to exclude it completely, as was done in ref10. However, this will significantly complicate the implementation and make it less understandable, so this should only be done if there is a real practical need. Currently, EdDSA signature verification in I2P is quite a rare event, compared to, say, ElGamal encryption.


There is an opinion that implementing your own cryptography is an extremely bad idea. However, using an implementation that is not clear as a working one seems to be little better. In addition, this article may be useful to those who are interested in getting to the bottom of it, and working practical code will help with this.

Tags:
Hubs:
Всего голосов 8: ↑8 и ↓0 +8
Комментарии 16
+16

Comments 16

A couple of points:
1) BerNstein
2) A bit offtopic. He also wrote script, which allows for a fairly clear comparison of different digital signature standards. You can see how similar they really are
The article is wonderful. I would like more details about the project itself.
Not "Berstein", but "Bernstein" (or "stein»?).
Thanks, fixed it.
However, using an implementation that is unclear how it works seems to be little better

There are quite a lot of completely understandable implementations. For example, tweetNaCl, written by the same Berstein (there are ports for it in other languages), if it is so difficult to read standard NaCl or libsodium code.

The problem with your implementation is, for example, that it does not take into account side channel attacks. “Transparent” implementations are useful only for understanding the work (although the usual formulas are enough for this), but in practice they cannot be used - this is worth writing about, and not about “don't implement your own crypto”»
>For example, tweetNaCl, written by the same Berstein
I also watched it while working. Yes, it is more understandable than ref10, but there are also a lot of ambiguities there. For example, why is 2^252-3 used there to extract the root, and not 2^252-2? What is D2? Why do you need to raise d^2+1 to the seventh power? Of course, there are answers to all these questions, but again this requires additional study of the issue.
Well, D2 is obviously twice the value of D.
The root needs to be taken not from a scalar (in this case it was really possible to raise it to the power of (p+3)/8), but from the fraction u/v - because this is the form in which the coordinates of a point are stored to speed up calculations. From here, most likely, the legs (p-5)/8 = (p+3)/8-1 and the seventh degree grow. All explanations are in the articles about Ed255 and Curve255

In any case, you need to understand it if the goal is “for practical use in I2P.” Your code may also be incomprehensible to someone: everyone has their own entry threshold. I'm not a real welder, but I think you can figure it out with TweetNaCl in a reasonable amount of time
Personally, I hope to eventually figure out ref10 and use it. You just need to go from simple to complex, and from code that works to code that works better..
Can you comment on the reasons for the fork from the main branch on Github??
Look at the history of changes in any file on Github - I'm no longer there. Some people took advantage of my temporary absence there, which looks extremely ugly on their part. When I returned, I decided to continue working on the project in my own fork, at the same time switching from crypto++ to openssl.
Hmm, how did you use it? I don’t see any particularly destructive actions there
There is no change history in all project files. It’s as if I haven’t done this project for two years and in general “you weren’t standing here.”».
One person was given all access rights to the project for completely different purposes.
Where is the fork? I couldn't find any mention.
I made a fork from release 0.10.0 from GitHub to bitbucket. Here
bitbucket.org/orignal/i2pd
Only full-fledged users can leave comments. Sign in, Please.